
package org.dea.mapping;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * This class provides functions for <code>Document</code> manipulation.
 * 
 * @author Guilherme Mauro Germoglio - guiga@ourgrid.org
 * @author Flavio Roberto Santos - flavio@ourgrid.org
 * @author Marcelo Iury - iury@ourgrid.org
 */
public class DocumentUtils {

    /**
     * Validate a xml document against his XmlSchema
     * 
     * @param strXml Xml file path
     * @param strSchema XmlSchema file path
     * @throws IOException
     * @throws SAXException
     */
    public static boolean validateXML( String xmlLocation, String schemaLocation ) throws SAXException, IOException {

        SchemaFactory schemaFactory = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI );

        if ( ( xmlLocation == null || xmlLocation.equals( "" ) ) || ( schemaLocation == null || schemaLocation.equals( "" ) ) )
            return false;

        Source schemaSource = new StreamSource( new File( schemaLocation ) );
        Schema schema = schemaFactory.newSchema( schemaSource );
        Validator validator = schema.newValidator();
        validator.validate( new StreamSource( xmlLocation ) );

        return true;
    }

    /**
     * This method implements an algorithm that compares two <code>Document</code> instances. The
     * comparison is restrict, that is, the documents are equals when they have correspondent nodes
     * with the same <code>getNodeName()</code> and <code>getNodeValue()</code>.
     * 
     * @param root1 The first document.
     * @param root2 The second document.
     * @return <code>true</code> if all the nodes have the same name and value, <code>false</code>
     *         otherwise.
     */
    public static boolean areEqual( Node root1, Node root2 ) {

        LinkedList<Node> list1 = new LinkedList<Node>();
        LinkedList<Node> list2 = new LinkedList<Node>();

        list1.add( root1 );
        list2.add( root2 );

        while ( !list1.isEmpty() && !list2.isEmpty() ) {
            Node a = ( Node ) list1.removeFirst();
            Node b = ( Node ) list2.removeFirst();

            if ( !compareNodes( a, b ) )
                return false;

            list1.addAll( nodeList2List( a.getChildNodes() ) );
            list2.addAll( nodeList2List( b.getChildNodes() ) );
        }

        return list1.isEmpty() && list2.isEmpty();
    }

    /**
     * Converts a <code>NodeList</code> to a <code>List</code>.
     * 
     * @param childNodes The node list to be converted.
     * @return The converted list.
     */
    public static List<Node> nodeList2List( NodeList childNodes ) {

        LinkedList<Node> list = new LinkedList<Node>();
        for ( int k = 0; k < childNodes.getLength(); k++ ) {
            list.addLast( childNodes.item( k ) );
        }
        return list;
    }

    /**
     * This method makes the tree clean. It means that the <code>Text</code> elements which have
     * an empty string as <code>getNodeValue().trim()</code> value will be removed from the tree.
     * 
     * @param rootNode The root of the tree.
     */
    public static void trim( Node rootNode ) {

        if ( rootNode != null ) {

            for ( int i = 0; i < rootNode.getChildNodes().getLength(); i++ ) {
                try {
                    trim( rootNode.getChildNodes().item( i ) );
                } catch ( RuntimeException e ) {
                    i--;
                }
            }

            if ( rootNode.getNodeType() == Node.TEXT_NODE ) {
                String value = rootNode.getNodeValue();

                if ( value.trim().equals( "" ) ) {
                    rootNode.getParentNode().removeChild( rootNode );
                    throw new RuntimeException();
                }
            }

        }
    }

    /**
     * Creates a new <code>Document</code> instance. If the <code>xmlLocation</code> is
     * <code>null</code>, the returned document is empty.
     * 
     * @param xmlLocation The path for the xml file.
     * @param namespaceAware If the DocumentBuilderFactory should be namespaceAware or not.
     * @return The created document.
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws Exception If some error occurs while loading the file.
     */
    public static Document createDocument( String xmlLocation, boolean namespaceAware ) throws IOException {

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        if ( xmlLocation == null || xmlLocation.equals( "" ) )
            try {
                return factory.newDocumentBuilder().newDocument();
            } catch ( ParserConfigurationException e ) {
                throw new RuntimeException( "Invalid or corrupted document!" );
            }
        factory.setNamespaceAware( namespaceAware );
        Document doc;
        try {
            doc = factory.newDocumentBuilder().parse( xmlLocation );
        } catch ( SAXException e ) {
            throw new RuntimeException( "Invalid or corrupted document!" );
        } catch ( ParserConfigurationException e ) {
            throw new RuntimeException( "Invalid or corrupted document!" );
        }
        factory = null;
        return doc;
    }

    /**
     * Creates a new <code>Document</code> instance.
     * 
     * @param xmlInputStream The xml Input stream.
     * @param namespaceAware If the DocumentBuilderFactory should be namespaceAware or not.
     * @return The created document.
     * @throws ParserConfigurationException 
     * @throws IOException 
     * @throws SAXException 
     * @throws Exception If some error occurs while loading the file.
     */
    public static Document createDocument( InputStream xmlInputStream, boolean namespaceAware ) throws IOException {

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware( namespaceAware );
        try {
            return factory.newDocumentBuilder().parse( xmlInputStream );
        } catch ( SAXException e ) {
            throw new RuntimeException( "Invalid or corrupted document!" );
        } catch ( ParserConfigurationException e ) {
            throw new RuntimeException( "Invalid or corrupted document!" );        }
    }

    /**
     * Compares two nodes.
     * 
     * @param a The first node.
     * @param b The second node.
     * @return <code>true</code> if the nodes have the same name and value, <code>false</code>
     *         otherwise.
     */
    private static boolean compareNodes( Node a, Node b ) {

        if ( a == null ^ b == null )
            return false;
        if ( a == null && b == null )
            return true;

        return compareNumberOfChildren( a, b ) && compareAttributes( a, b ) && compareStrings( a.getNodeName(), b.getNodeName() ) && compareStrings( a.getNodeValue(), b.getNodeValue() );
    }

    /**
     * Compares the number of Children from two <code>Node</code>s.
     * 
     * @param a A <code>Node</code>.
     * @param b Another <code>Node</code>.
     * @return <code>true</code> case the number of children from the two <code>Node</code>.
     *         <code>false</code> otherwise.
     */
    private static boolean compareNumberOfChildren( Node a, Node b ) {

        return a.getChildNodes().getLength() == b.getChildNodes().getLength();
    }

    /**
     * Compares the attributes from two nodes.
     * 
     * @param a A <code>Node</code>.
     * @param b Another <code>Node</code>.
     * @return <code>true</code> case the list of attributes from each node are the same.
     *         <code>false</code> otherwise.
     */
    private static boolean compareAttributes( Node a, Node b ) {

        if ( a.hasAttributes() != b.hasAttributes() ) {
            return false;
        }
        if ( !a.hasAttributes() && !b.hasAttributes() ) {
            return true;
        }
        if ( a.getAttributes().getLength() != b.getAttributes().getLength() ) {
            return false;
        }

        for ( int k = 0; k < a.getAttributes().getLength(); k++ ) {
            if ( !compareNodes( a.getAttributes().item( k ), b.getAttributes().item( k ) ) ) {
                return false;
            }
        }
        return true;
    }

    /**
     * Compares two string. Note that if both of them are <code>null</code>, they are equals.
     * 
     * @param a The first string.
     * @param b The second string.
     * @return <code>true</code> if the string are equals, ,<code>false</code> otherwise.
     */
    private static boolean compareStrings( String a, String b ) {

        if ( a == null ^ b == null )
            return false;
        if ( a == null && b == null )
            return true;
        return a.equals( b );
    }

    /**
     * Prints the Document to a file.
     * 
     * @param The root Node of the Document.
     * @param outputFile The file name where the Document will be written.
     * @throws FileNotFoundException 
     * @throws Exception Case it is impossible to write the Document.
     */
    public static void printOutDocument( Document root, String outputFile ) throws FileNotFoundException {

        TransformerFactory tf = TransformerFactory.newInstance();
        javax.xml.transform.Transformer transformer;
        try {
            transformer = tf.newTransformer();
            transformer.transform( new DOMSource( root ), new StreamResult( new FileOutputStream( outputFile ) ) );
        } catch ( TransformerException e ) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * Returns a list containing all the nodes that have the specified <code>namespace</code> and
     * <code>localName</code>. The search starts from the <code>root</code> element.
     * 
     * @param root The node where the search starts from.
     * @param namespace The namespace.
     * @param localName The localName.
     * @return A list containing the found nodes.
     */
    public static List getNodes( Node root, String localName ) {

        List<Node> result = new LinkedList<Node>();
        List<Node> elementList;
        try {
            elementList = getAllDescendants( root );
        } catch ( Exception e ) {
            return result;
        }

        for ( int k = 0; k < elementList.size(); k++ ) {
            Node node = ( Node ) elementList.get( k );
            Element element = ( Element ) node;

            if ( element.getNodeName().equals( localName ) ) {
                result.add( element );
            }
        }

        return result;
    }

    /**
     * Returns a list containing all the nodes that have the specified <code>namespace</code> and
     * <code>localName</code>. The search starts from the <code>root</code> element.
     * 
     * @param root The node where the search starts from.
     * @param namespace The namespace.
     * @param localName The localName.
     * @return A list containing the found nodes.
     */
    public static List getNodesByName( Node root, String nodeType, String nodeAttributeName ) {

        List<Node> result = new LinkedList<Node>();
        List<Node> elementList;
        try {
            elementList = getAllDescendants( root );
        } catch ( Exception e ) {
            return result;
        }

        for ( int k = 0; k < elementList.size(); k++ ) {
            Node node = ( Node ) elementList.get( k );
            Element element = ( Element ) node;

            if ( element.getNodeName().equals( nodeType ) ) {
                if ( element.getAttribute( "name" ).equals( nodeAttributeName ) )
                    result.add( element );
            }
        }

        return result;
    }

    /**
     * Returns a list containing all the nodes that have the specified <code>namespace</code> and
     * <code>localName</code>. The search starts from the <code>root</code> element.
     * 
     * @param root The node where the search starts from.
     * @param localName The localName.
     * @return A list containing the found nodes.
     */
    public static List getNodes( Node root, String namespace, String localName ) {

        List<Node> result = new LinkedList<Node>();

        List<Node> elementList;
        try {
            elementList = getAllDescendants( root );
        } catch ( Exception e ) {
            return result;
        }

        for ( int k = 0; k < elementList.size(); k++ ) {
            Node node = ( Node ) elementList.get( k );
            Element element = ( Element ) node;

            if ( namespace.equals( element.getNamespaceURI() ) && element.getLocalName().equals( localName ) ) {
                result.add( element );
            }
        }

        return result;
    }

    /**
     * Returns the String value of a <code>Node</code>.
     * 
     * @param node The <code>Node</code> to have the value returned.
     * @return A String that is the value of the <code>Node</code>.
     */
    public static String getValue( Node node ) {

        if ( node.getNodeType() == Node.TEXT_NODE ) {
            return node.getNodeValue();
        }
        DocumentUtils.trim( node );
        String value = "";
        NodeList nodeList = node.getChildNodes();
        for ( int k = 0; k < nodeList.getLength(); k++ ) {
            value += DocumentUtils.getValue( nodeList.item( k ) );
        }
        return value;
    }

    /**
     * Returns all <code>Element</code>s that are descendant of the <code>Node</code>
     * specified.
     * 
     * @param node The <code>Node</code>.
     * @return A <code>List</code> of all <code>Element</code>s that are descendant of the
     *         <code>Node</code> specified.
     */
    public static List<Node> getAllDescendants( Node node ) {

        LinkedList<Node> list = new LinkedList<Node>();
        LinkedList<Node> toVisit = new LinkedList<Node>();
        for ( int k = 0; k < node.getChildNodes().getLength(); k++ ) {
            toVisit.add( node.getChildNodes().item( k ) );
        }

        while ( !toVisit.isEmpty() ) {
            Node visitedNode = ( Node ) toVisit.removeFirst();

            for ( int k = 0; k < visitedNode.getChildNodes().getLength(); k++ ) {
                toVisit.add( visitedNode.getChildNodes().item( k ) );
            }

            if ( visitedNode.getNodeType() == Node.ELEMENT_NODE ) {
                list.add( visitedNode );
            }
        }
        return list;
    }

}
